import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:sqliteasy_core/sqliteasy_core.dart' hide ColumnInfo, ForeignKeyInfo, TableInfo, IndexInfo;
import 'package:source_gen/source_gen.dart';
import '../../model/code_fragment.dart';
import '../../model/database.dart';
import '../../errors.dart';

import '../../const.dart';
import '../database_util.dart';

class ValueConverterGenerator {
  static TypeChecker _ignoreTypeChecker = TypeChecker.fromRuntime(Ignore);

  static CodeFragment genImpl(DartType type) {
    if (type?.element is! ClassElement) {
      return null;
    }

    final classElement = type.element as ClassElement;
    final entityInfo = DatabaseUtil.getEntityInfoFromElement(classElement);
    final columnMap = Map<String, ColumnInfo>();
    entityInfo.columns.forEach((element) {
      columnMap[element.field.name] = element;
    });

    final constructor = _getConstructor(classElement);
    final constructorDeclared = classElement.constructors?.isNotEmpty == true;

    if (constructor != null) {
      // Check the params
      constructor.parameters.forEach((element) {
        if (!columnMap.containsKey(element.name)) {
          throw SQLiteasyBuildError("The parameter name of the constructor of the entity(${classElement.name}) must be the same as one of the column field");
        }
      });
    } else if (constructorDeclared) {
      throw SQLiteasyBuildError("There is no available constructor for Entity(${classElement.name})");
    }

    final retTypeName = type.toString().replaceAll("*", "");
    final content = """
    class ${DatabaseUtil.getValueConverterName(entityInfo)} extends ValueConverter<${retTypeName}> {
        const ${DatabaseUtil.getValueConverterName(entityInfo)}();
        List<${retTypeName}> convert(input) {
          final ret = <${retTypeName}>[];
          final doConvertFunc = (dataMap) {
            ${_genConvertCode(columnMap, entityInfo, constructor)}
          };
          if (input is List) {
            ret.addAll(input.map((e) => doConvertFunc(e)));
          } else {
            ret.add(doConvertFunc(input));
          }
          return ret;
        }
    }
    """;
    return CodeFragment(content, [entityInfo.location, Const.packageCore]);
  }

  static ConstructorElement _getConstructor(ClassElement classElement) {
    if (classElement.constructors?.isNotEmpty != true) {
      return null;
    }
    final constructors = classElement.constructors;

    ConstructorElement ret;
    constructors.forEach((element) {
      if (!_ignoreTypeChecker.hasAnnotationOf(element)) {
        if (ret != null) {
          throw SQLiteasyBuildError(
              "There can only be one constructor for an entity class. If there is more than one, please mark other constructors with Ignore annotation.");
        }
        ret = element;
      }
    });
    return ret;
  }

  static String _genConvertCode(Map<String, ColumnInfo> columnMap, EntityInfo entityInfo, ConstructorElement constructor) {
    List<String> constructorFieldsCode = List<String>();
    Set<String> generatedFields = Set();
    constructor.parameters.forEach((element) {
      constructorFieldsCode.add(_genColumnConvertCode(columnMap[element.name], true));
      generatedFields.add(element.name);
    });

    List<String> otherFieldsCode = List<String>();
    columnMap.forEach((key, value) {
      if (!generatedFields.contains(key)) {
        otherFieldsCode.add(_genColumnConvertCode(value, false));
      }
    });
    final code = """
    ${constructorFieldsCode.join("\n")}
    ${entityInfo.entityName} entity = ${entityInfo.entityName}(${constructor?.parameters?.map((e) => e.name)?.join(",") ?? ""});
    ${otherFieldsCode.join("\n")}
    return entity;
    """;

    return code;
  }

  static String _genColumnConvertCode(ColumnInfo info, bool isConstructorField) {
    final fieldType = info.field.type;
    final typeKey = fieldType.toString().replaceAll("*", "");
    if (isConstructorField) {
      return "";
    }
    return "entity.${info.field.name} = TypeHelper.deserializeByTypeKey(\"$typeKey\", dataMap[\"${info.name}\"]);";
  }
}
